🔨Balanced Search Trees

在许多应用中,二叉搜索树可以工作得很好,然而在最差情况下(插入一个有序序列),它的性能得不到保证。下面介绍一种平衡搜索树,可以在任何情况下保证 O(logN) 的时间复杂度。

理想情况下,我们希望树的形状是完美平衡的,这样的话,查找效率就是 O(lgN);然而,对于不可预料的动态插入操作,保持树形状完美平衡的开销太大。我们稍稍放松对完美平衡的要求,便可以对所有操作提供 O(logN) 的保证

2-3搜索树(2-3 search trees)

我们现在允许树的一个节点中有两个值(Key),连接着三个子节点(Link),将这样的节点称为3-节点,原来那样的节点称为2-节点

————————————————————————

一个2-3搜索树要么是空的,要么是:

  • 一个 2-节点,左链接连接着一棵2-3搜索树(所有的值小于当前节点的值),右链接连接着一棵2-3搜索树(所有的值大于当前结点的值)

  • 一个 3-节点,左链接连接着一棵2-3搜索树(所有的值小于当前节点中较小的那个值),中链接连接着一棵2-3搜索树(所有值在当前结点两个值之间),右链接连接着一棵2-3搜索树(所有的值大于当前结点较大的那个值)

————————————————————————

如果一个2-3搜索树的所有空链接到根节点的距离都相等,那么我们就说这棵2-3搜索树是平衡的。

————————————————————————

搜索(Search)

从根节点开始向下比较,如果与节点中任一值相等,则搜索命中;如果不相等,我们按照左中右连接进行分流;如果遇到空节点,则说明查找失败。

插入到一个 2-节点 中(Insert into a 2-node)

用一个 3-节点 替换掉原来的 2-节点

插入到只含一个3-节点的2-3树中(Insert into a tree consisting of a single 3-node)

把3-节点变成一个暂时的4-节点,然后把这个4-节点分成三个2-节点

插入到一个父节点是2-节点的3-节点中(Insert into a 3-node whose parent is a 2-node)

把3-节点变成一个暂时的4-节点,然后把这个4-节点分成三个2-节点,将父节点变为一个3-节点

插入到一个父节点是3-节点的3-节点中(Insert into a 3-node whose parent is a 3-node)

向上把每个3-节点变成一个暂时的4-节点,然后把这个4-节点分成三个2-节点,将父节点变为一个暂时的4-节点。由此向上,直到遇到一个2-节点,或到根节点时让整个树增加1层。

怎样分开一个4-节点

————————————————————————

对于一个含有N个元素的2-3树,搜索和插入操作保证最多访问 lgN 个节点

————————————————————————

红黑树(Red-Black BSTs)

编码 3-节点

最自然的方法是利用二分查找树(BST),然后添加一些信息来标记 3-节点

我们把链接分为两种:

  • 红链接(Red Link):连接着两个2-节点来表示一个3-节点
  • 黑链接(Black Link):和BST的链接一样,连接着2-3树

特别地,我们用向左倾斜的红链接连接着的两个2-节点来表示一个3-节点

————————————————————————

红黑树是满足下列调节的具有红黑链接的二叉树:

  • 红链接向左倾斜
  • 没有一个节点连接着两条红链接
  • 红黑树是平衡的:每个空链接到根节点的路径上有相同数量的黑链接

————————————————————————

红黑树即是2-3树,也是BST,所以它结合了BST的高效搜索方法和2-3树的平衡插入特点

颜色表示

在节点的定义里加入一个 bool 变量来表示当前结点与父节点的链接是不是红链接

旋转(Rotation)

在红黑书的实现中,我们暂时允许以下情况:

  • 向右倾斜的红链接
  • 一个节点连接着两个红链接

我们用旋转来修正第一种情况

Node rotateLeft(Node h)
{
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    x.color = h.color;
    h.color = RED;
    x.N = h.N;
    h.N = 1 + size(h.left) + size(h.right);
    return x;
}

Node rotateRight(Node h)
{
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    x.color = h.color;
    h.color = RED;
    x.N = h.N;
    h.N = 1 + size(h.left) + size(h.right);
    return x;
}

插入到一个 2-节点 中(Insert into a 2-node)

如果新插入的节点比原来的节点小,则将它正常插入,然后把新节点的颜色设为红色;如果比原来的大,则正常插入,把新节点的颜色设置为红色,然后进行一次左旋转。

插入到只含一个3-节点的2-3树中(Insert into a tree consisting of a single 3-node)

void flipColors(Node h)
{
    h.color = RED;
    h.left.color = BLACK;
    h.right.color = BLACK;
}

保持根节点是黑色的

上面的代码有可能会使根节点的颜色变为红色,所以每次插入操作后,我们要让根节点的颜色变回黑色。注意到,每当根节点颜色变红时,树的高度就增加了1。

插入到一个位于底部的 3-节点中(Insert into a 3-node at the bottom)

相当于将一个新的红链接逐层向上传递,直到符合顺序(没有右倾斜的红链接,没有两个红链接连着同一个节点等),或到达根节点。

插入操作的实现

public void put(Key key, Value val)
{
    root = put(root, key, val);
    root.color = BLACK;
}

private Node put(Node h, Key key, Value val)
{
    if (h == null)    return new Node(key, val, 1, RED);

    int cmp = key.compareTo(h.key);
    if        (cmp < 0) h.left  = put(h.left,  key, val);
    else if (cmp > 0) h.right = put(h.right, key, val);
    else h.val = val;

    if (isRed(h.right) && !isRed(h.left))    h = rotateLeft(h);
    if (isRed(h.left) && isRed(h.left.left) h = rotateRight(h);
    if (isRed(h.left) && isRed(h.right))    flipColors(h);

    h.N = size(h.left) + size(h.right) + 1;
    return h;
}

删除